我們今天就來繼續更深入Django的MTV架構,看看有哪些細節是我們需要再注意的
以下程式碼一樣:https://github.com/class83108/django_project/tree/hello_world
今日的重點:
路由可以配置變數,而配置變數的種類以下幾種:
# 根目錄的urls.py
from .views import index_view
urlpatterns = [
....
path("<str:user_name>/<int:age>/<slug:page_name>/<uuid:user_id>", index_view),
]
# 在根目錄建立views.py
from django.shortcuts import render
def index_view(request, user_name, age, page_name, user_id):
# 需要在視圖的參數中直接寫上變數名稱,跟get請求中參數不同,不要搞混
url_info = {
"user_name": user_name,
"user_name_type": type(user_name).__name__,
"age": age,
"age_type": type(age).__name__,
"page_name": page_name,
"page_name_type": type(page_name).__name__,
"user_id": user_id,
"user_id_type": type(user_id).__name__,
}
return render(request, "index.html", {"url_info": url_info})
# 在templates目錄下建立index.html
<body>
<div>
{% for key, value in url_info.items %}
<p>{{ key }}: {{ value }}</p>
{% endfor %}
</div>
</body>
然後我們在瀏覽器就可以看到
那path的部分
# 根目錄下的urls.py
from .views import index_view, demo_path_view
urlpatterns = [
...
path("<path:to_inde_page>", demo_path_view),
]
# 根目錄下的views.py
def demo_path_view(request, to_inde_page):
return render(
request,
"index.html",
{
"to_inde_page": to_inde_page,
"to_inde_page_type": type(to_inde_page).__name__,
},
)
# templates下的index.html
<p>
{{ to_inde_page }}: {{ to_inde_page_type }}
</p>
可以看到可以匹配到更靈活的url
那如果想要匹配更加靈活的話,可以使用到re_path作為url的配置,也就是正規表達式。只是因為在一般狀況下path就能滿足大部分需求,因此這邊就不展開說明
想使用可以去看官方文檔:
https://docs.djangoproject.com/en/5.1/ref/urls/#django.urls.re_path
如果網頁的規模提升,路由的數量一多起來,不但更難管理,程式碼的可讀性就會下降
藉由對路由進行命名,開發時更能了解這個路由具備什麼樣的功能
# 根目錄下的url.py
urlpatterns = [
...
path("article/", include(("article.urls", "article"), namespace="article")),
...
]
# 我們為article這個app下所有路由的空間命名為article(namespace)
# article.urls後面的article不能為空,通常直接設為app名稱,如果不寫會出現以下警告
django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported. Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.
# article下的urls.py
urlpatterns = [
...
path("tag/<int:tag_id>/", tag_detail_view, name="tag_detail_view"),
]
# 將路由命名為tag_tetail_view
在templates與views中使用命名的方式如下
# templates
<div>
<a href="{% url "article:tag_detail_view" "1" %}">tag_detail_view</a>
<p>the link of the above url: <strong>{% url "article:tag_detail_view" "1" %}</strong></p>
</div>
# url內的規則為 <namespace>:<url_name>
# 如果有變數就是放後面,有多個的話就是如下
# "" "" ""
# views.py
from django.urls import reverse
def demo_view(request):
url = reverse("article:tag_detail_view", args=[1])
return render(request, "demo.html", {"url": url})
我們可以很輕易的判斷這會是在article app下的標籤詳情頁,因此增加程式碼的可讀性
如果團隊對於命名有一定規範的,應該更能體現這樣命名的優勢
並且也能透過resolve
方法,來取得路由對象,獲取路由的詳細資訊
詳細可以看官方文檔:https://docs.djangoproject.com/en/5.1/ref/urlresolvers/#resolve
在前面的案例中有一些模版的基本配置,這邊我們展開來繼續說
首先介紹Django內建的常用模板標籤
# HTML file
# 遍歷可迭代對象
{% for item in items %}
{{ item }}
{% endfor %}
# 條件判斷
{% if url %}
<p>{{ url }}</p>
{% elif not url %}
<p>url is not defined</p>
{% else %}
{% endif %}
# 生成csrf_token標籤
{% csrf_token %}
# 生成對應的url
{% url "" %}
# 將變數重新命名
{% with %}
# 載入Django的標籤,不論是內建或是自定義還是第三方的
{% load xxx %} # {% load staic %} {% load staticfiles %}
# 讀取靜態檔案
{% static "css/style.css" %}
# 模板繼承
{% extends "base.html" %}
# 表示載入特定模板
{% include "card.html" %}
# 重寫父模板(繼承模板)中的特定區塊
{% block xxx %}
{% endblock xxx %}
我們在前面的範例中,可以看到很多時候就是一個視圖搭配一個HTML,然後裡面的HTML又臭又長。簡單的規模是還好,但是如果網站規模一起來可想而知會有多難管理。
如果是有前端開發經驗的讀者,一定對於管理Component不陌生,雖然Django本身沒有類似State的管理機制,但是透過繼承、傳遞參數與條件判斷,也能做出類似props的效果,方便大量的模板
以下進入範例:
# templates下建立base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Base</title>
{% block extra_header %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
{% endblock extra_header %}
</head>
<body>
{% block content %}{% endblock content %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% endblock extra_js %}
</body>
</html>
我們在基礎頁面中添加了Bootstrap的css與js,並且把他們包在特定的block中,當然如果整個站點的所有網站都會需要用到Bootstrap可以不用這樣做,只是單純增加彈性而已
如果想要改寫block內容的同時,又不會刪除原有的內容,可以這樣寫
{% block extra_header %}
{{ block.super }}
{% endblock %}
OK~我們繼續
# urls.py
urlpatterns = [
...
path("demo_html_tag/", demo_html_tag_view),
]
# views.py
def demo_html_tag_view(request):
samoyed_content = "The Samoyed is a breed of large herding dog that descended from the Nenets herding laika, a spitz-type dog, with a thick, white, double-layer coat."
golden_retriever_content = "The Golden Retriever is a medium-large gun dog that was bred to retrieve shot waterfowl, such as ducks and upland game birds, during hunting and shooting parties."
return render(
request,
"demo_html_tag.html",
{
"samoyed_content": samoyed_content,
"golden_retriever_content": golden_retriever_content,
},
)
然後在static下建立images資料夾,並且放入我們等一下會用到的圖片,圖片都是用unsplash的免費素材
接著我們回到我們的主角templates們
# templates下建立demo_html_tag.html
{% extends "base.html" %}
{% block content %}
<section class="card_container container">
<div class="row">
<div class="col-lg-6">
{% include "card.html" with title="Samoyed Dogs" content=samoyed_content img="images/samoyed.jpg" %}
</div>
<div class="col-lg-6">
{% include "card.html" with title="Golden Retriever" content=golden_retriever_content img="images/golden.jpg" %}
</div>
</div>
</section>
{% endblock content %}
# templates下建立card.html
{% load static %}
<div class="card" style="width: 18rem;">
{% if img %}
<img src="{% static img %}" class="card-img-top" alt="...">
{% else %}
<img src="https://via.placeholder.com/150" class="card-img-top" alt="...">
{% endif %}
<div class="card-body">
{% if title %}
<h5 class="card-title">{{ title }}</h5>
{% else %}
<h5 class="card-title">Card title</h5>
{% endif %}
{% if content %}
<p class="card-text">{{ content }}</p>
{% else %}
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
{% endif %}
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
好~我們來解釋一下我們都做什麼
首先templates資料夾會是像是這樣
base.html
是大家的基礎模板,demo_html_tag.html
則是要渲染的頁面,而card.html
則是像元件一樣的角色
所以在demo_html_tag.html
中,我們需要繼承父模板的資料,使用到extends
標籤。接著我們想要客製化每一張card自己的內容,所以除了將card.html
進行include
之外,也進行了傳參
最後在card中,為了增加元件的可用性,會需要對變數的存在做基本的條件判斷
最後來到瀏覽器,就可以看到我們可愛的兩張卡片啦
Django的模板透過繼承、包含與傳參等等作用,讓我們在做開發時,即使增加頁面的數量,也能在app下分別建立template方便管理大量的模板,並且也不需要很高的學習門檻就能看懂程式碼的運作
其他更多的模板標籤可以參考官方文檔:
https://docs.djangoproject.com/en/5.1/ref/templates/builtins/
並且也能夠自定義自己的標籤模板,過濾器等等,這邊也不一一說明了,有興趣的就來讀文檔吧XD
https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/
在前面已經有稍微提過Model中可以設置不同的欄位類型,這邊就不再贅述
我們會針對幾個部分來進行介紹:
class DemoModel(models.Model):
name = models.CharField(max_length=120)
age = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
is_deleted = models.BooleanField(default=False)
category = models.ForeignKey("Category", on_delete=models.CASCADE)
author = models.ForeignKey("Author", on_delete=models.CASCADE)
tags = models.ManyToManyField("Tag")
class Meta:
db_table = "demo_model" # 設置資料表名稱
verbose_name = "Demo Model" # 設置在admin後台顯示的名稱
verbose_name_plural = "Demo Model" # 設置在admin後台顯示的名稱(複數形式)
ordering = ["-created_at"] # 設置資料排序方式 - 表示降序,默認表示升序
abstract = True # 設置為抽象模型,不會生成資料表
unique_together = ["name", "age"] # 設置唯一索引
indexes = [models.Index(fields=["name", "age"])] # 設置索引
permissions = [("can_read_demo_model", "Can read demo model")] # 設置權限
app_label = "article" # 設置應用名稱 如果不設置則默認為應用名稱
def __str__(self):
return self.name # 返回模型實例的名稱
def save(self, *args, **kwargs):
# 可以自定義保存邏輯
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
# 可以自定義刪除邏輯
super().delete(*args, **kwargs)
def clean(self) -> None:
# 可以自定義驗證邏輯 例如驗證name是否為空 通常用於表單驗證
if not self.name:
raise ValueError("name cannot be empty")
return super().clean()
def get_absolute_url(self):
# 返回模型實例的絕對URL
return f"/demo_model/{self.pk}/"
除此之外,也能夠過封裝的方法,來讓某些方法能夠被複用
class DemoManager(models.Manager):
def is_active(self):
return self.filter(is_active=True)
class DemoModel(models.Model):
...
objects = DemoManager()
在上面的DemoManger中,我們通過定義is_active方法,來做到:
Demo.objects.filter(is_active=True)
Demo.objects.is_active()
透過在Model中做好設定,可以在視圖函式的撰寫中減少許多重複的邏輯寫法
尤其是clean()與save()方法,在開發上增加了更多的靈活度
在後面介紹form跟admin的時候,會做更多的示範,到時後就能體會到Django在處理數據上在自由度與封裝度都給出了很多自由,讓開發者能更專注在功能開發上
只是要注意如果是使用批量插入數據時例如bulk_create()
不會觸發save方法,所以在使用一些Queryset API
上時需要特別注意,詳情可以看官方文檔:
https://docs.djangoproject.com/en/4.2/ref/models/querysets/#bulk-create
雖然Django中也能支援寫SQL原生語法(在一些很複雜的語句上),但是在大部分狀況下,ORM語法已經能支援大部分的開發情境,以下是一些基本的ORM操作
def article_view(request):
articles = Article.objects.all() # 取得所有文章
article = Article.objects.get(article_id=1) # 取得文章 id=1 的文章
article, created = Article.objects.get_or_create(
title="Hello", content="World"
) # 取得或新增文章
articles = Article.objects.filter(title__contains="Hello") # 篩選文章
articles = Article.objects.exclude(title__contains="Hello") # 排除文章
articles = Article.objects.order_by("-created_at") # 排序文章
articles = Article.objects.order_by("created_at")[0:2] # 取得前兩篇文章
return render(request, "article.html", {"articles": articles})
def create_or_update_article(request):
if request.method == "POST":
title = request.POST["title"]
content = request.POST["content"]
try:
article = get_object_or_404(Article, title=title)
article.content = request.POST["content"]
article.save()
except:
article = Article.objects.create(title=title, content=content)
return render(request, "article_detail.html", locals())
from django.shortcuts import render, get_object_or_404
def delete_article(request, pk):
article = get_object_or_404(Article, pk=pk)
article.delete()
return redirect('article_list')
from django.db.models import Q
def search_articles(request):
article_query = request.GET.get("article_query")
articles = Article.objects.filter(
Q(title__contains=article_query) | Q(content__contains=article_query)
)
class Article(models.Model):
...
category = models.ForeignKey("Category", on_delete=models.CASCADE, related_name='articles')
author = models.ForeignKey("Author", on_delete=models.CASCADE, related_name='articles')
tags = models.ManyToManyField("Tag", related_name='articles')
# 獲取某個分類的所有文章
category = Category.objects.get(category_id=1)
articles_in_category = category.articles.all()
# 獲取某個作者的所有文章
author = Author.objects.get(author_id=1)
articles_by_author = author.articles.all()
# 獲取包含某個標籤的所有文章
tag = Tag.objects.get(tag_id=1)
articles_with_tag = tag.articles.all()
# 預先加載 category 和 author,減少數據庫查詢
articles = Article.objects.select_related('category', 'author').all()
for article in articles:
print(f"{article.title} - {article.category.name} by {article.author.name}")
# 預先加載 tags,減少數據庫查詢
articles = Article.objects.prefetch_related('tags').all()
for article in articles:
print(f"{article.title} - Tags: {', '.join(tag.name for tag in article.tags.all())}")
上述例子展示了 Django ORM 在處理複雜關聯查詢時的強大功能,可以有效地進行數據檢索和操作,同時保持程式碼的簡潔和可讀性
我們今天更加深入了解到Django MTV結構下,不同單元之間是怎麼協同合作的
但是有關Model的部分還是有許多部分可以再深入的探討,明天我們會針對多資料庫,以及如何動態的添加模型做更深入的探討